/*******************************************************************************
        
        copyright:		Copyright (C) Mike Reinhold 2010. All rights reserved
        				
        license:		BSD style: $(LICENSE)
        		
        author(s): 		Mike Reinhold and Phil DeMonaco.
        				
*******************************************************************************/
module tango.util.cipher.BlockCryptor;

import tango.util.cipher.Cipher;
import tango.util.cipher.Cryptor;
import tango.util.cipher.modes.CBC;
import tango.util.cipher.order.NoOrdering;
import tango.util.cipher.pad.NoPadding;

/** Default values for unspecified parameters, must be imported above */
alias CBC DefMode;
alias NoOrdering DefInOrdering;
alias NoOrdering DefOutOrdering;
alias NoPadding DefPadding;

/** Cryptor implementation for wrapping block ciphers */ 
class BlockCryptor: Cryptor {

	private {
		SymmetricKey _key;
		IVParameters _iv;
		BlockCipherPadding _pad;
		BlockCipherOrdering _inord;
		BlockCipherOrdering _outord;
		BlockCipherMode _crypt;
		bool _encrypt_ = true;
	}

	/***************************************************************************
			encrypt
			
			Returns:
			If the Cryptor is in encrypt mode
			   
	***************************************************************************/
	bool encrypt() {
		return _encrypt_;
	}

	/***************************************************************************
			init
			
			Params:
			cipher_ = The Block cipher to wrap
			key_ = The symmetric key
			iv_ = The initialization vector
			mode_ = The block cipher operation mode
			pad_ = The block padding implementation
			inord_ = The block reordering for the input blocks
			outord_ = The block reordering for the output blocks
			
			Remarks:
			Initialize this BlockCryptor with the specified parameters   
			
	***************************************************************************/
	void init(BlockCipher cipher_, SymmetricKey key_, IVParameters iv_ = null,
			BlockCipherMode mode_ = null, BlockCipherPadding pad_ = null,
			BlockCipherOrdering inord_ = null,
			BlockCipherOrdering outord_ = null) {

		//use the provided mode if available
		if(mode_ is null) {
			_crypt = new DefMode(cipher_);
		} else {
			_crypt = mode_;
			_crypt.cipher(cipher_);
		}

		if(key_ is null) invalid("Cryptor: Key cannot be null");
		
		//resize the key material and initialization vector
		key_.setPreferredSize(cipher_);
		iv_.setIVSize(cipher_);
		_key = key_;
		_iv = iv_;

		//initialize the block cipher mode
		_crypt.init(Cipher.ENCRYPT, key_, iv_);
		_encrypt_ = Cipher.ENCRYPT;

		//use the provided padding method if available
		if(pad_ is null) {
			_pad = new DefPadding();
		} else {
			_pad = pad_;
		}

		//use the provided input and output orderings when available
		if(inord_ is null) {
			_inord = new DefInOrdering();
		} else {
			_inord = inord_;
		}

		if(outord_ is null) {
			_outord = new DefOutOrdering();
		} else {
			_outord = outord_;
		}
		
		if(!_pad.allowsReordering){
			if(!(cast(NoOrdering)_outord && cast(NoOrdering)_inord)){
				invalid("Cryptor: " ~ name ~ ": Padding scheme does not allow reordering");
			}
		}
		
		if(_crypt.requiresPadding()){
			if(cast(NoPadding)_pad){
				invalid("Cryptor: " ~ name ~ ": Block Cipher requires padding");
			}
		}
	}

	/***************************************************************************
			
			init
				
			Remarks:
			Reset the BlockCryptor and the wrapped cipher. If the wrapped cipher has
			not been initialized an exception will be thrown.
			
	***************************************************************************/
	void reset() {
		if(_crypt is null) invalid("Cryptor: Not initialized");
		_crypt.reset();
	}

	/***************************************************************************
			
			name
				
			Returns:
			The name of the cryptor.
			
	***************************************************************************/
	string name() {
		if(_crypt is null) invalid("Cryptor: Not initialized");
		return _crypt.name() ~ "/" ~ _pad.name ~ "/" ~ _inord.name ~ "/" ~ _outord.name;
	}

	/***************************************************************************
			
			encrypt
			
			Params:
			input_ = plaintext to be encrypted
			resetPrior = If the Cryptor should be reset prior to encrypting
				
			Returns:
			ciphertext output of the cryptor
			
			Remarks:
			encrypts the input plaintext using the preset cipher. If the
			cipher has not been initialized an exeception will be thrown. 
			
	***************************************************************************/
	void[] encrypt(void[] input_, bool resetPrior = true) {
		ubyte[] input = cast(ubyte[]) input_;
		uint padLen, numBlocks;
		
		if(_crypt is null) invalid("Cryptor: Not initialized");
		
		//prepare
		if(resetPrior) {// || !encrypt) {
			_crypt.reset();										
			_crypt.init(Cipher.ENCRYPT, _key, _iv);
			_encrypt_ = Cipher.ENCRYPT;
		}
		
		//calc the pad length, number of blocks, and pad the input string
		numBlocks = input.length / _crypt.blockSize + 1;
		padLen = numBlocks * _crypt.blockSize - input.length;
		input ~= _pad.pad(padLen);

		//create the output string
		void[] output_ = new void[input.length];

		//encrypt each block, with input and output reordering
		for(uint i = 0; i < numBlocks; i++) {
			uint pos = _inord.order(i, numBlocks) * _crypt.blockSize;
			uint pos2 = _outord.order(i, numBlocks) * _crypt.blockSize;
			_crypt.update(input[pos .. input.length], 
					output_[pos2 .. output_.length]); 
		}

		//return the ciphertext
		return output_;
	}

	/***************************************************************************
	
			decrypt
			
			Params:
			input_ = ciphertext to be decrypted
			resetPrior = If the Cryptor should be reset prior to decrypting
				
			Returns:
			plaintext output of the cryptor
			
			Remarks:
			decrypts the input ciphertext using the preset cipher. If the
			cipher has not been initialized an exeception will be thrown. 
	
	***************************************************************************/
	void[] decrypt(void[] input_, bool resetPrior = false) {
		void[] output_ = new void[input_.length];
		uint padLen, numBlocks;

		if(_crypt is null) invalid("Cryptor: Not initialized");
		
		//prepare
		if(resetPrior) {// || encrypt) {
			_crypt.reset();	
			_crypt.init(Cipher.DECRYPT, _key, _iv);
			_encrypt_ = Cipher.DECRYPT;
		}

		//calc the number of blocks
		numBlocks = (input_.length / _crypt.blockSize) +
					((input_.length % _crypt.blockSize) ? 1 : 0);

		//decrypt each block
		for(uint i = 0; i < numBlocks; i++) {
			uint pos = _inord.order(i, numBlocks) * _crypt.blockSize;
			uint pos2 = _outord.order(i, numBlocks) * _crypt.blockSize;
			_crypt.update(input_[pos2 .. input_.length],
					output_[pos .. output_.length]); 	
		}

		//unpad and return the plaintext
		padLen = _pad.unpad(output_);
		return output_[0 .. output_.length - padLen];
	}
	
	BlockCryptor dup() {
		BlockCryptor b = new BlockCryptor();
		b._key = new SymmetricKey(this._key);
		b._iv = new IVParameters(this._iv);
		
		b._pad = this._pad.dup;
		b._inord = this._inord.dup;
		b._outord = this._outord.dup;
		
		//TODO Fix the copy on this /*or maybe these don't???*/
		b._crypt = this._crypt.dup;		//but this one definitely does
		
		return b;
	}
}